#ifndef FILE_H_INCLUDED
#define FILE_H_INCLUDED

#include "FilePath.h"
#include <climits>

namespace CEngine
{

typedef U64 FilePosition;

///Define the file handle to be whatever type the OS uses.
#ifdef WINDOWS
    typedef void* FileHandle;
#endif

///Generic file flags,
#define CE_FILE_ACCESS_READ             0x1         ///Allows read access to the file.
#define CE_FILE_ACCESS_WRITE            0x2         ///Allows write access to the file.
#define CE_FILE_CREATE_NEW              0x4         ///Will will create a file and fail if it already exists.
#define CE_FILE_CREATE_ALWAYS           0x8         ///Always creates a file.
#define CE_FILE_OPEN_EXISTING           0xC         ///Opens an existing file and fails if it does not exist.
#define CE_FILE_OPEN_ALWAYS             0x10        ///Opens an existing file or creates a new one.
#define CE_FILE_TRUNCATE_EXISTING       0x14        ///Opens an existing file and destroys the contents.
#define CE_FILE_READ_ONLY               0x20        ///Sets the file to be read only.
#define CE_FILE_DELETE_ON_CLOSE         0x40        ///The file will deleted when it is completely closed.
#define CE_FILE_DONT_BUFFER             0x80        ///The OS will not buffer file access (use with caution).
#define CE_FILE_LOCK                    0x100       ///Does not allow anything else to access the file.
#define CE_FILE_ASCII                   0x200       ///Will treat the data as text instead of bytes.
///OS specific file flags.
#ifdef WINDOWS      ///OSs that don't use these flags should define them as 0.
    #define CE_FILE_HIDDEN                 0x400    ///Sets the file as hidden.
    #define CE_FILE_RANDOM_ACCESS          0x800    ///OS caching optimized for random access.
    #define CE_FILE_SEQUENTIAL_ACCESS      0x1000   ///OS caching optimized for sequential access.
    #define CE_FILE_WRITE_THROUGH          0x2000   ///Write operations are immediately written bypassing the cache.
#endif

///Error codes.
#define CE_ERROR_NONE                   0x0         ///No error.
#define CE_ERROR_FILE_EXISTS            0x1         ///The file already exists.
#define CE_ERROR_FILE_NOT_FOUND         0x2         ///The file was not found.
#define CE_ERROR_CANNOT_READ            0x3         ///The file cannot be read.
#define CE_ERROR_CANNOT_WRITE           0x4         ///The file cannot be written to.
#define CE_ERROR_FILE_LOCKED            0x5         ///The file could not be opened due to it already being locked.
#define CE_ERROR_FILE_NOT_OPEN          0x6         ///The file is not open.
#define CE_ERROR_FILE_ALREADY_OPEN      0x7         ///The file is already open.
#define CE_ERROR_EOF                    0x8         ///The end of the file was reached.
#define CE_ERROR_OTHER                  0xFF        ///Other error.

class File {
    public:

        ///specifies where 0 is mapped to when seeking through the file.
        enum SeekMode {
            BEGINNING,
            CURRENT,
            END
        };

        ///Creates a file class with no path, and default flags set.
        File(void);

        ///Creates a file class with the given path, and either the default flags or custom flags.
        ///File has not been created yet!
        File(FilePath path,
             const U32 flags = CE_FILE_OPEN_EXISTING | CE_FILE_ACCESS_READ |
                               CE_FILE_ACCESS_WRITE | CE_FILE_LOCK);

        ///Destructor.
        ~File(void);

        ///Copy operator.
        File&
        operator=(const File& rhs);

        ///Sets the path to the given FilePath.
        void
        setPath(const FilePath& path);

        ///Gets the file's path.
        FilePath
        getPath(void) const;

        ///Gets the file's size in bytes.
        U64
        getSize(void);

        ///Gets the current file read position.
        FilePosition
        getReadPosition(void) const;

        ///Sets the read position relative to the seek mode.
        void
        setReadPosition(const I64 position, const SeekMode = CURRENT);

        ///Gets the current file write position.
        FilePosition
        getWritePosition(void) const;

        ///Sets the write position relative to the seek mode.
        void
        setWritePosition(const I64 position, const SeekMode = CURRENT);

        ///Sets the flags (only has an effect when opening the file.
        void
        setFlags(const U32 flags);

        ///Gets the flags for the file.
        U32
        getFlags(void) const;

        ///Returns if the file is open or not.
        bool
        isOpen(void) const;

        ///Returns the file's error code if any.
        U8
        getError(void) const;

        ///Clears the file's error code.
        void
        clearError(void);

        ///Creates and/or opens the file. Returns if the file opened successfully, if not the error code is set,
        ///and the system error is printed to the console. Fails if the file is already open.
        bool
        open(void);

        ///Closes the file, and flushes the buffer. Returns if the file closed successfully,
        ///otherwise the error code is set, and the system error is printed. Succeeds if the file is already closed.
        bool
        close(void);

        ///Copies the data from the file of length into the output buffer (must be at least 1 byte bigger than length).
        ///When in ASCII mode, stops at either newlines (or the given delimiter) or \0, and discards them.
        ///Returns the number of bytes read. If the file could not be read or EOF is reached the error code is set.
        U64
        read(U8* buffer, const U64 length, const U8 delimiter = '\0');

        ///Copies the input buffer of length into a 64kB buffer, and writes it when full.
        ///When in ASCII mode converts \r or \n to the correct newline character. Can be forced to flush the buffer.
        ///returns the number of bytes written. If the file could not be written to the error code is set.
        U64
        write(U8* buffer, const U64 length, const bool flush = false);

        ///Returns if the file is open or not. Allows for things like if(file) {...}
        explicit operator bool(void) const;

    private:

        U64             size;
        FilePosition    readPosition;
        FilePosition    writePosition;
        FilePosition    readBufferBeginning;
        FilePosition    readBufferEnding;
        FilePath        path;
        FileHandle      handle;
        U8*             writeBuffer;
        U8*             readBuffer;
        U32             flags;
        U16             writeBufferPosition;
        bool            fileIsOpen;
        U8              errorCode;

        void printSystemError(U32 error);
        U64 calculateSize(void);
};

inline void
File::setPath(const FilePath& p) {
    path = p;
}

inline FilePath
File::getPath(void) const {
    return path;
}

inline U64
File::getSize(void) {
    return calculateSize();
}

inline FilePosition
File::getReadPosition(void) const {
    return readPosition;
}

inline void
File::setReadPosition(const I64 position, const SeekMode mode) {
    switch(mode) {
        case BEGINNING: if(position < 0) {
                            readPosition = 0;
                            break;
                        }
                        readPosition = position;
                        break;
        case CURRENT: if(readPosition + position < 0) {
                            readPosition = 0;
                            break;
                      }
                      //else if(position > 0 && readPosition + position < readPosition) {
                            //readPosition = ULLONG_MAX; Enable if this becomes a thing.
                      //}
                      readPosition += position;
                      break;
        case END: if(position > 0) {
                        readPosition = size;
                        break;
                  }
                  readPosition += position;
                  break;
    }
}

inline FilePosition
File::getWritePosition(void) const {
    return writePosition;
}

inline void
File::setWritePosition(const I64 position, const SeekMode mode) {
    switch(mode) {
        case BEGINNING: if(position < 0) {
                            writePosition = 0;
                            break;
                        }
                        writePosition = position;
                        break;
        case CURRENT: if(writePosition + position < 0) {
                            writePosition = 0;
                            break;
                      }
                      //else if(position > 0 && writePosition + position < writePosition) {
                            //writePosition = ULLONG_MAX; Enable if this becomes a thing.
                      //}
                      writePosition += position;
                      break;
        case END: if(position > 0) {
                        writePosition = size;
                        break;
                  }
                  writePosition += position;
                  break;
    }
}

inline void
File::setFlags(const U32 f) {
    flags = f;
}

inline U32
File::getFlags(void) const {
    return flags;
}

inline bool
File::isOpen(void) const {
    return fileIsOpen;
}

inline U8
File::getError(void) const {
    return errorCode;
}

inline void
File::clearError(void) {
    errorCode = 0;
}

inline
File::operator bool() const {
    if(fileIsOpen) {
        return true;
    }
    return false;
}

}

#endif // FILE_H_INCLUDED
